# -*- coding: utf-8 -*-
#-------------------------------------------------------------------------------
# Name:        data_cache.py
# Purpose:     GAEDrive Client: data cache on Non Volatile Memory, and RAM
#
# Author:      Crane Chu <cranechu (at) gmail.org>
#
# Created:     Nov. 20, 2009
# Copyright:   Copyright 2009,2010 Crane Chu
#
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#-------------------------------------------------------------------------------
#!/usr/bin/env python

__author__ = 'Crane Chu'


# MODULES
import os
from gaedrive_config import CHUNK_SIZE, CACHE_SIZE


# INDEX of chunk tuple: (url, key)
CHUNK_URL = 0
CHUNK_KEY = 1


class DATACache(object):
    def __init__(self, net_io, cache_path):
        self.net_io = net_io
        self._write_dir = cache_path + '/.write/'
        self._read_dir = cache_path + '/.read/'
        # init the dir of data cache
        for dir in (self._write_dir, self._read_dir):
            if not os.path.exists(dir):
                os.mkdir(dir)
    
    def _findFile(self, chunk_url, chunk_key):
        # try write dir
        filename = self._write_dir + chunk_url + ':' + chunk_key
        if not os.path.exists(filename):
            filename = None
            
        # try read dir
        if not filename:
            filename = self._read_dir + chunk_url + ':' + chunk_key
            if not os.path.exists(filename):
                filename = None
        
        return filename
    
    def _reclaim(self):
        # delete chunk files in read dir, depends on its time stamps
        files = os.listdir(self._read_dir)
        file_list = [(os.path.getatime(self._read_dir + '/' + file),
                      os.path.getctime(self._read_dir + '/' + file),
                      file) for file in files]
        file_list.sort()
        # release 1/5 nvm space, calcate the number of file to be deleted
        nf = CACHE_SIZE // 5 // CHUNK_SIZE
        for file in file_list[:nf]:
            os.unlink(self._read_dir + '/' + file)
    
    def _readChunk(self, chunk_url, chunk_key):
        # try write dir
        data = None
        filename = self._write_dir + chunk_url + ':' + chunk_key
        if os.path.exists(filename):
            f = open(filename, 'rb')
            data = f.read()
            f.close()
        
        return data
    
    def _storeChunk(self, chunk_url, chunk_key, data):
        """ write the whole chunk data
        """
        ret = False
        while ret is False:
            try:
                # write to read dir
                filename = self._read_dir + chunk_url + ':' + chunk_key
                f = open(filename, 'wb')
                f.write(data)
                f.close()
                
                # delete the same chunk in write dir if possible
                filename = self._write_dir + chunk_url + ':' + chunk_key
                if os.path.exists(filename):
                    os.remove(filename)
                
                ret = True
            except:
                # no space in NVM, reclaim the space of NVM
                self._reclaim()
                ret = False

    def read(self, chunk, size, offset, rand):
        """ read one chunk
        RAM, NVM, and then net_io
        """
        data = None
        chunk_url = chunk[CHUNK_URL]
        chunk_key = chunk[CHUNK_KEY]

        filename = self._findFile(chunk_url, chunk_key)        
        # return the chunk data
        if filename:
            f = open(filename, 'rb')
            data = f.read()
            f.close()
        else:
            # get data from net_io
            data = self.net_io.readChunk(chunk_url, chunk_key, rand)
            if data:
                # save in local data cache
                self._storeChunk(chunk_url, chunk_key, data)
        
        return data[offset:offset + size]
    
    def write(self, chunk, offset, data):
        """ write data (within one chunk) to file
        write to RAM, or NVM. Flush to net_io by bg threads
        """
        ret = False
        chunk_url = chunk[CHUNK_URL]
        chunk_key = chunk[CHUNK_KEY]

        while ret is False:
            try:
                # write to write dir
                filename = self._write_dir + chunk_url + ':' + chunk_key
                f = open(filename, 'ab')
                f.seek(offset)
                f.write(data)
                f.close()
                
                # delete the same chunk in read dir if possible
                filename = self._read_dir + chunk_url + ':' + chunk_key
                if os.path.exists(filename):
                    os.remove(filename)
                
                ret = True
            except:
                # no space in NVM, reclaim the space of NVM
                self._reclaim()
                ret = False
        else:
            ret = False
            
        return ret
    
    def truncate(self, chunk, length):
        # truncate one chunk
        chunk_url = chunk[CHUNK_URL]
        chunk_key = chunk[CHUNK_KEY]

        # truncate or delete chunks in local cache
        filename = self._findFile(chunk_url, chunk_key)  
        if filename:
            f = open(filename, 'r+b')
            ret = f.truncate(length)
            f.close()
        else:
            ret = False
            
        return ret

    def delete(self, chunks):
        ret = True
        for chunk in chunks:
            chunk_url = chunk[CHUNK_URL]
            chunk_key = chunk[CHUNK_KEY]
            
            filename = self._findFile(chunk_url, chunk_key)  
            if filename:
                ret = os.unlink(filename)
            else:
                ret = False
            
        return ret

    def flush(self, chunk, rand):
        # flush a chunk in NVM to GAE
        ret = False
        chunk_url = chunk[CHUNK_URL]
        chunk_key = chunk[CHUNK_KEY]
        data = self._readChunk(chunk_url, chunk_key)
        if data and self.net_io.writeChunk(chunk_url, chunk_key, data, rand):
            ret = self._storeChunk(chunk_url, chunk_key, data)

        return ret
        
    def clear(self):
        """ clear all chunks in read dir
        """
        files = os.listdir(self._read_dir)
        for file in files:
            os.unlink(self._read_dir + file)
        os.rmdir(self._read_dir)
        os.mkdir(self._read_dir)
